Skip to content

fix: keep log viewer pinned during drawer resize and minimize/reexpand - #48

Merged
joshuapare merged 4 commits into
mainfrom
fix/log-scroll-pinning
Mar 28, 2026
Merged

fix: keep log viewer pinned during drawer resize and minimize/reexpand#48
joshuapare merged 4 commits into
mainfrom
fix/log-scroll-pinning

Conversation

@joshuapare

@joshuapare joshuapare commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a ResizeObserver on the log viewer scroll container to detect drawer size changes
  • When follow mode is active, automatically re-pin to the last log entry during resize (drag, expand, fullscreen)
  • When follow mode is off and the drawer is restored from minimized, save and restore the first visible line index so scroll position is preserved

Tickets

  • Bug: Resizing bottom bar doesn't keep logs pinned to bottom
  • Bug: Log view resets to top after minimize/reexpand

Verification

  • pnpm build succeeds
  • pnpm lint passes (no new errors — all errors are pre-existing)

Summary by CodeRabbit

  • Bug Fixes
    • Improved auto-scroll behavior in the log viewer when toggling follow mode so new entries stay in view as expected.
    • Restored and preserved precise scroll position when the log panel expands or collapses, returning you to the previously visible log line.
    • Smoother, more reliable scroll handling during panel resize to reduce jumps and maintain expected view position.

…e position after minimize/reexpand

Add a ResizeObserver on the log scroll container that re-pins to the
last entry when follow mode is active during any container size change
(drag resize, minimize/expand, fullscreen toggle). When follow mode is
off and the drawer is restored from minimized, the first visible line
index is saved and restored so the user's scroll position is preserved.
@coderabbitai

coderabbitai Bot commented Mar 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces single scroll ref with a stateful scrollElement via scrollRefCallback, adds a ResizeObserver on that element, and uses ref mirrors (followRef, filteredLineCountRef, filteredEntriesRef) to: auto-scroll in follow mode, save start index when collapsing, and restore it when expanding. Updates JSX ref usage.

Changes

Cohort / File(s) Summary
LogViewer Scroll & Resize Handling
ui/providers/BottomDrawer/containers/LogViewer/index.tsx
Replaced parentRef usage with scrollElement state and scrollRefCallback; added ResizeObserver effect observing container height changes; added ref mirrors followRef, filteredLineCountRef, filteredEntriesRef; implemented save/restore of first-visible entry timestamp when container collapses/expands; auto-scroll to last filtered entry when follow=true; disconnects observer on unmount.

Sequence Diagram(s)

sequenceDiagram
    participant RO as ResizeObserver
    participant LV as LogViewer
    participant V as Virtualizer
    participant DOM as ScrollContainer

    RO->>LV: notify resize (height change)
    LV->>LV: read followRef, filteredLineCountRef, filteredEntriesRef, lastContainerHeight
    alt follow === true and DOM.height > 0 and filteredEntries > 0
        LV->>V: scrollToIndex(lastFilteredIndex)
        V->>DOM: update scroll position
    else follow === false and prevHeight === 0 and DOM.height > 0
        LV->>LV: resolve savedAnchorTimestamp -> index via findEntryIndexByTime
        LV->>V: scrollToIndex(restoredIndex)
        V->>DOM: update scroll position
    else follow === false and DOM.height === 0 and prevHeight > 0
        V->>LV: provide current startIndex
        LV->>LV: savedAnchorTimestamp = timestampOf(startIndex)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop where viewports shrink and grow,
I tuck a timestamp when the drawer goes low.
When follow beckons, I bound to the end,
When opened again, I find where you penned.
Nibble, restore — your log-reading friend.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: fixing log viewer scroll pinning during drawer resize and minimize/reexpand operations, which aligns with the PR's core objectives of addressing scroll position issues.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/log-scroll-pinning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/providers/BottomDrawer/containers/LogViewer/index.tsx`:
- Around line 189-194: The follow-mode branch resets the auto-scroll guard too
early by using a single requestAnimationFrame; update the block that sets
isAutoScrolling.current = true, calls
rowVirtualizer.scrollToIndex(savedFirstVisibleRef.current, { align: 'start' }),
and clears savedFirstVisibleRef.current to use a double requestAnimationFrame
before setting isAutoScrolling.current = false so the scroll handler has settled
(i.e., wrap the current requestAnimationFrame callback in another
requestAnimationFrame), keeping references to isAutoScrolling.current,
rowVirtualizer.scrollToIndex, and savedFirstVisibleRef.current.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 17052e90-94f7-4f16-be86-d31e8f70c952

📥 Commits

Reviewing files that changed from the base of the PR and between dd6cd05 and 9590e6b.

📒 Files selected for processing (1)
  • ui/providers/BottomDrawer/containers/LogViewer/index.tsx

Comment thread ui/providers/BottomDrawer/containers/LogViewer/index.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/providers/BottomDrawer/containers/LogViewer/index.tsx`:
- Around line 167-169: The effect that creates/attaches the ResizeObserver bails
out when parentRef.current is null, so the observer never attaches if the scroll
viewport mounts later; change the attachment logic to track the actual scroll
element via a state-backed or callback ref (e.g., create scrollElement state and
set it from a ref callback) and move the ResizeObserver setup/cleanup into a
useEffect that depends on scrollElement (or parentRef.current) so the observer
is (re-)attached when the viewport mounts; update any code that uses parentRef
(follow-mode repinning / restore-from-minimize) to reference the new
scrollElement ref/state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2ce28cab-0654-4e63-aaf3-d46b6b878310

📥 Commits

Reviewing files that changed from the base of the PR and between 9590e6b and 04dd6db.

📒 Files selected for processing (1)
  • ui/providers/BottomDrawer/containers/LogViewer/index.tsx

Comment thread ui/providers/BottomDrawer/containers/LogViewer/index.tsx Outdated
…r mounts late

The scroll container only renders once log entries arrive. Using a
callback ref + state ensures the ResizeObserver effect re-runs when
the element actually appears in the DOM, rather than bailing out on
initial mount when parentRef.current is still null.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/providers/BottomDrawer/containers/LogViewer/index.tsx`:
- Around line 169-170: savedFirstVisibleRef currently stores a mutable array
index (range.startIndex) which becomes invalid when filteredEntries reindexes
due to buffer eviction or filter changes; change the ref to store a stable
anchor (e.g., the LogEntry.timestamp or lineNumber of the first visible entry)
instead of an index, update the save site (where savedFirstVisibleRef is set
around lines 205-208) to capture entry.timestamp (or entry.lineNumber), and
update the restore site (where the ref is read around lines 191-195) to resolve
that anchor back to the current index by using the existing findEntryIndexByTime
(or analogous lookup) or jumpToTime logic against filteredEntries; adjust the
ref declaration (around lines 169-170) to use useRef<number | null> for the
timestamp/lineNumber type and ensure fallbacks if the anchor is not found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 29b91be7-d2ca-4de1-b16f-6fbc12b7831b

📥 Commits

Reviewing files that changed from the base of the PR and between 04dd6db and 4fd7c6b.

📒 Files selected for processing (1)
  • ui/providers/BottomDrawer/containers/LogViewer/index.tsx

Comment thread ui/providers/BottomDrawer/containers/LogViewer/index.tsx Outdated
Store the first visible entry's timestamp (stable across buffer
eviction) rather than its array index. On restore, resolve the
timestamp back to the current index via findEntryIndexByTime so
the scroll position survives head eviction while minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/providers/BottomDrawer/containers/LogViewer/index.tsx`:
- Line 172: The savedVisibleTimestampRef currently stores only entry.timestamp
which makes restore ambiguous for duplicate timestamps; change the saved anchor
to a composite key (e.g., timestamp + sourceId + lineNumber) wherever
savedVisibleTimestampRef is set and read, then on restore attempt to locate the
exact entry by that composite (match timestamp, sourceId, lineNumber) before
falling back to findEntryIndexByTime(targetTimestamp). Update the code paths
that set/read savedVisibleTimestampRef (the savedVisibleTimestampRef declaration
and its setters/consumers around the restore logic) and ensure the restore logic
resolves the exact row first and only uses the timestamp-only lookup if that
composite entry is missing due to eviction or filtering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1adcd218-cff9-4ec2-a7c0-ba03e33c27ba

📥 Commits

Reviewing files that changed from the base of the PR and between 4fd7c6b and e483d23.

📒 Files selected for processing (1)
  • ui/providers/BottomDrawer/containers/LogViewer/index.tsx

// Re-pin to bottom (or restore position) when the scroll container resizes.
// This covers drawer drag-resize, minimize/re-expand, and fullscreen toggle.
// We store a timestamp (not an index) so the anchor survives buffer eviction.
const savedVisibleTimestampRef = useRef<string | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Anchor restore to a unique log entry.

Saving only entry.timestamp makes this restore ambiguous: findEntryIndexByTime() returns the first row with timestamp >= target, so duplicate timestamps snap back to the earliest sibling instead of the row that was actually visible. Save a composite anchor for the visible entry (for example timestamp + sourceId + lineNumber) and resolve that exact row first, falling back to the timestamp lookup only if the entry has been evicted or filtered out.

🔧 Suggested change
-  // We store a timestamp (not an index) so the anchor survives buffer eviction.
-  const savedVisibleTimestampRef = useRef<string | null>(null);
+  // Store a stable row identity so restore stays exact across eviction/filter changes.
+  const savedVisibleAnchorRef =
+    useRef<Pick<LogEntry, 'timestamp' | 'sourceId' | 'lineNumber'> | null>(null);

...
-        } else if (prevHeight === 0 && savedVisibleTimestampRef.current !== null) {
+        } else if (prevHeight === 0 && savedVisibleAnchorRef.current !== null) {
+          const anchor = savedVisibleAnchorRef.current;
           const idx = findEntryIndexByTime(
             filteredEntriesRef.current,
-            new Date(savedVisibleTimestampRef.current),
+            new Date(anchor.timestamp),
           );
-          savedVisibleTimestampRef.current = null;
-          if (idx >= 0) {
+          let restoreIdx = idx;
+          for (let i = idx; i >= 0 && i < filteredEntriesRef.current.length; i += 1) {
+            const candidate = filteredEntriesRef.current[i];
+            if (candidate.timestamp !== anchor.timestamp) break;
+            if (
+              candidate.sourceId === anchor.sourceId
+              && candidate.lineNumber === anchor.lineNumber
+            ) {
+              restoreIdx = i;
+              break;
+            }
+          }
+          savedVisibleAnchorRef.current = null;
+          if (restoreIdx >= 0) {
             isAutoScrolling.current = true;
-            rowVirtualizer.scrollToIndex(idx, { align: 'start' });
+            rowVirtualizer.scrollToIndex(restoreIdx, { align: 'start' });
             requestAnimationFrame(() => {
               requestAnimationFrame(() => {
                 isAutoScrolling.current = false;
               });
             });
           }
         }
...
-          if (entry?.timestamp) {
-            savedVisibleTimestampRef.current = entry.timestamp;
+          if (entry?.timestamp) {
+            savedVisibleAnchorRef.current = {
+              timestamp: entry.timestamp,
+              sourceId: entry.sourceId,
+              lineNumber: entry.lineNumber,
+            };
           }

Also applies to: 194-203, 217-219

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/providers/BottomDrawer/containers/LogViewer/index.tsx` at line 172, The
savedVisibleTimestampRef currently stores only entry.timestamp which makes
restore ambiguous for duplicate timestamps; change the saved anchor to a
composite key (e.g., timestamp + sourceId + lineNumber) wherever
savedVisibleTimestampRef is set and read, then on restore attempt to locate the
exact entry by that composite (match timestamp, sourceId, lineNumber) before
falling back to findEntryIndexByTime(targetTimestamp). Update the code paths
that set/read savedVisibleTimestampRef (the savedVisibleTimestampRef declaration
and its setters/consumers around the restore logic) and ensure the restore logic
resolves the exact row first and only uses the timestamp-only lookup if that
composite entry is missing due to eviction or filtering.

@joshuapare
joshuapare merged commit 89cf055 into main Mar 28, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant